package com.googlecode.horncomb.orm.tool.codegen.ant;

import java.io.*;
import java.util.*;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.taskdefs.MatchingTask;

import com.googlecode.horncomb.orm.tool.codegen.FileConverter;

import freemarker.ext.ant.JythonAntTask;
import freemarker.template.utility.ClassUtil;
import freemarker.template.utility.SecurityUtilities;
import freemarker.template.*;

/**
 * 这是一个Ant任务用于处理FreeMarker模板，并生成结果。
 * <p>
 * 属性列表:
 * <table border="1" cellspacing="0" cellpadding="2">
 * <tr>
 * <th align="left" valign="top">属性</th> <th align="left" valign="top">描述</th>
 * <th valign="top" width="50">必填</th>
 * </tr>
 * <tr>
 * <td valign="top">basedir</td>
 * <td valign="top">命令执行的基础目录</td>
 * <td align="center" valign="top">No</td>
 * </tr>
 * <tr>
 * <td valign="top">output</td>
 * <td valign="top">结果输出目录</td>
 * <td align="center" valign="top">yes</td>
 * </tr>
 * <tr>
 * <td valign="top">fileSuffix</td>
 * <td valign="top">输出结果文件的后缀（包括扩展名）</td>
 * <td align="center" valign="top">No</td>
 * </tr>
 * <tr>
 * <td valign="top">template</td>
 * <td valign="top">模板文件名</td>
 * <td align="center" valign="top">No</td>
 * </tr>
 * <tr>
 * <td valign="top">templateDir</td>
 * <td valign="top">模板所在目录</td>
 * <td align="center" valign="top">No</td>
 * </tr>
 * <tr>
 * <td valign="top">encoding</td>
 * <td valign="top">编码输出结果编码</td>
 * <td align="center" valign="top">No</td>
 * </tr>
 * <tr>
 * <td valign="top">templateEncoding</td>
 * <td valign="top">模板编码</td>
 * <td align="center" valign="top">No</td>
 * </tr>
 * <tr>
 * <td valign="top">processer</td>
 * <td valign="top">处理器，可选的有：
 * cn.horncomb.freemarker.support.SqlMapProcesser，cn.horncomb
 * .freemarker.support.CommonFileProcesser 。</td>
 * <td align="center" valign="top">yes</td>
 * </tr>
 * </table>
 */
public class FreemarkerCodeGenerator extends MatchingTask {
	private JythonAntTask prepareModel;
	private JythonAntTask prepareEnvironment;
	private final DocumentBuilderFactory builderFactory;
	private DocumentBuilder builder;

	/** 处理器 */
	private String processer;

	/** the {@link Configuration} used by this task. */
	private Configuration cfg = new Configuration();

	/** 生成的目标目录 */
	private File output;

	/** 匹配的文件的正则 */
	private String fileMatch = ".*";

	/** 基础目录 */
	private File basedir;

	/** Where the templates live */
	private File templateDir;

	/** the template= attribute */
	private String templateName;

	/** The template in its parsed form */
	private Template parsedTemplate;

	/** last modified of the template sheet */
	private long templateFileLastModified = 0;

	/** the projectFile= attribute */
	private String projectAttribute = null;

	private File projectFile = null;

	/** The DOM tree of the project wrapped into FreeMarker TemplateModel */
	private TemplateModel projectTemplate;
	// The DOM tree wrapped using the freemarker.ext.dom wrapping.
	private TemplateNodeModel projectNode;
	private TemplateModel propertiesTemplate;
	private TemplateModel userPropertiesTemplate;

	/** last modified of the project file if it exists */
	private long projectFileLastModified = 0;

	/** check the last modified date on files. defaults to true */
	private boolean incremental = true;

	/** 文件输出的结尾（也包括扩展名） */
	private String fileSuffix = ".xml";

	private String encoding = SecurityUtilities
			.getSystemProperty("file.encoding");
	private String templateEncoding = encoding;
	private boolean validation = false;

	private String models = "";
	private final HashMap<String, Object> modelsMap = new HashMap<String, Object>();

	/**
	 * Constructor creates the SAXBuilder.
	 */
	public FreemarkerCodeGenerator() {
		builderFactory = DocumentBuilderFactory.newInstance();
		builderFactory.setNamespaceAware(true);
	}

	/**
	 * Set the destination directory into which the generated files should be
	 * copied to
	 * @param dir
	 *            the name of the destination directory
	 */
	public void setDestdir(File dir) {
		output = dir;
	}

	/**
	 * Set the output file extension. <tt>.html</tt> by default.
	 */
	public void setExtension(String extension) {
		this.fileSuffix = extension;
	}

	public void setTemplate(String templateName) {
		this.templateName = templateName;
	}

	public void setTemplateDir(File templateDir) throws BuildException {
		this.templateDir = templateDir;
		try {
			cfg.setDirectoryForTemplateLoading(templateDir);
		} catch (Exception e) {
			throw new BuildException(e);
		}
	}

	/**
	 * Set the path to the project XML file
	 */
	public void setProjectfile(String projectAttribute) {
		this.projectAttribute = projectAttribute;
	}

	/**
	 * Turn on/off incremental processing. On by default
	 */
	public void setIncremental(String incremental) {
		this.incremental = !(incremental.equalsIgnoreCase("false")
				|| incremental.equalsIgnoreCase("no") || incremental
				.equalsIgnoreCase("off"));
	}

	/**
	 * Set encoding for generated files. Defaults to platform default encoding.
	 */
	public void setEncoding(String encoding) {
		this.encoding = encoding;
	}

	public void setTemplateEncoding(String inputEncoding) {
		this.templateEncoding = inputEncoding;
	}

	/**
	 * Sets whether to validate the XML input.
	 */
	public void setValidation(boolean validation) {
		this.validation = validation;
	}

	public void setModels(String models) {
		this.models = models;
	}

	public void execute() throws BuildException {
		DirectoryScanner scanner;
		String[] list;

		if (basedir == null) {
			basedir = getProject().getBaseDir();
		}
		if (output == null) {
			String msg = "destdir attribute must be set!";
			throw new BuildException(msg, getLocation());
		}

		File templateFile = null;

		if (templateDir == null) {
			if (templateName != null) {
				// 直接在全局路径上查找模板
				templateFile = new File(templateName);
				if (!templateFile.isAbsolute()) {
					templateFile = new File(getProject().getBaseDir(),
							templateName);
				}
				templateDir = templateFile.getParentFile();
				templateName = templateFile.getName();
			} else {
				templateDir = basedir;
			}
			setTemplateDir(templateDir);
		} else if (templateName != null) {
			if (new File(templateName).isAbsolute()) {
				throw new BuildException(
						"Do not specify an absolute location for the template as well as a templateDir");
			}
			templateFile = new File(templateDir, templateName);
		}
		if (templateFile != null) {
			templateFileLastModified = templateFile.lastModified();
		}

		try {
			if (templateName != null) {
				parsedTemplate = cfg
						.getTemplate(templateName, templateEncoding);
			}
		} catch (IOException ioe) {
			throw new BuildException(ioe.toString());
		}
		// get the last modification of the template
		log("结果输出: " + output.getAbsolutePath(), Project.MSG_INFO);

		// projectFile relative to basedir
		if (projectAttribute != null && projectAttribute.length() > 0) {
			projectFile = new File(basedir, projectAttribute);
			if (projectFile.isFile())
				projectFileLastModified = projectFile.lastModified();
			else {
				log("Project file is defined, but could not be located: "
						+ projectFile.getAbsolutePath(), Project.MSG_INFO);
				projectFile = null;
			}
		}

		generateModels();

		// find the files/directories
		scanner = getDirectoryScanner(basedir);

		propertiesTemplate = wrapMap(getProject().getProperties());
		userPropertiesTemplate = wrapMap(getProject().getUserProperties());

		builderFactory.setValidating(validation);
		try {
			builder = builderFactory.newDocumentBuilder();
		} catch (ParserConfigurationException e) {
			throw new BuildException("Could not create document builder", e,
					getLocation());
		}

		// get a list of files to work on
		list = scanner.getIncludedFiles();

		for (int i = 0; i < list.length; ++i) {
			try {
				if (list[i].matches(this.fileMatch)) {
					process(basedir, list[i], output);
				}
			} catch (Exception ex) {
				throw new BuildException(ex);
			}
		}
	}

	public void addConfiguredJython(JythonAntTask jythonAntTask) {
		this.prepareEnvironment = jythonAntTask;
	}

	public void addConfiguredPrepareModel(JythonAntTask prepareModel) {
		this.prepareModel = prepareModel;
	}

	public void addConfiguredPrepareEnvironment(JythonAntTask prepareEnvironment) {
		this.prepareEnvironment = prepareEnvironment;
	}

	/**
	 * Process an XML file using FreeMarker
	 * @throws ClassNotFoundException
	 * @throws IllegalAccessException
	 * @throws InstantiationException
	 */
	private void process(File baseDir, String inFileStr, File destDir)
			throws BuildException, ClassNotFoundException,
			InstantiationException, IllegalAccessException {

		if (processer == null)
			return;
		FileConverter proc = (FileConverter) Class.forName(processer)
				.newInstance();
		proc.process(baseDir, inFileStr, destDir, this);
		this.log("文件" + inFileStr + "处理完成", Project.MSG_INFO);

	}

	private void generateModels() {
		StringTokenizer modelTokenizer = new StringTokenizer(models, ",; ");
		while (modelTokenizer.hasMoreTokens()) {
			String modelSpec = modelTokenizer.nextToken();
			String name = null;
			String clazz = null;

			int sep = modelSpec.indexOf('=');
			if (sep == -1) {
				// No explicit name - use unqualified class name
				clazz = modelSpec;
				int dot = clazz.lastIndexOf('.');
				if (dot == -1) {
					// clazz in the default package
					name = clazz;
				} else {
					name = clazz.substring(dot + 1);
				}
			} else {
				name = modelSpec.substring(0, sep);
				clazz = modelSpec.substring(sep + 1);
			}
			try {
				modelsMap.put(name, ClassUtil.forName(clazz).newInstance());
			} catch (Exception e) {
				throw new BuildException(e);
			}
		}
	}

	/**
	 * create directories as needed
	 */
	public void ensureDirectoryFor(File targetFile) throws BuildException {
		File directory = new File(targetFile.getParent());
		if (!directory.exists()) {
			if (!directory.mkdirs()) {
				throw new BuildException("Unable to create directory: "
						+ directory.getAbsolutePath(), getLocation());
			}
		}
	}

	@SuppressWarnings("unchecked")
	private static TemplateModel wrapMap(Map table) {
		SimpleHash model = new SimpleHash();
		for (Iterator<String> it = table.keySet().iterator(); it.hasNext();) {
			Object key = it.next();
			Object value = table.get(key);
			model.put(key.toString(), new SimpleScalar(value.toString()));
		}
		return model;
	}

	@SuppressWarnings("unchecked")
	public void insertDefaults(Map root) {
		root.put("properties", propertiesTemplate);
		root.put("userProperties", userPropertiesTemplate);
		if (projectTemplate != null) {
			root.put("project", projectTemplate);
			root.put("project_node", projectNode);
		}
		if (modelsMap.size() > 0) {
			for (Iterator it = modelsMap.entrySet().iterator(); it.hasNext();) {
				Map.Entry entry = (Map.Entry) it.next();
				root.put(entry.getKey(), entry.getValue());
			}
		}
	}

	public File getBasedir() {
		return basedir;
	}

	public void setBasedir(File baseDir) {
		this.basedir = baseDir;
	}

	public DocumentBuilder getBuilder() {
		return builder;
	}

	public void setBuilder(DocumentBuilder builder) {
		this.builder = builder;
	}

	public DocumentBuilderFactory getBuilderFactory() {
		return builderFactory;
	}

	public Configuration getCfg() {
		return cfg;
	}

	public void setCfg(Configuration cfg) {
		this.cfg = cfg;
	}

	public File getOutput() {
		return output;
	}

	public void setOutput(File destDir) {
		this.output = destDir;
	}

	public boolean isIncremental() {
		return incremental;
	}

	public void setIncremental(boolean incremental) {
		this.incremental = incremental;
	}

	public Map<String, Object> getModelsMap() {
		return modelsMap;
	}

	public Template getParsedTemplate() {
		return parsedTemplate;
	}

	public void setParsedTemplate(Template parsedTemplate) {
		this.parsedTemplate = parsedTemplate;
	}

	public JythonAntTask getPrepareEnvironment() {
		return prepareEnvironment;
	}

	public void setPrepareEnvironment(JythonAntTask prepareEnvironment) {
		this.prepareEnvironment = prepareEnvironment;
	}

	public JythonAntTask getPrepareModel() {
		return prepareModel;
	}

	public void setPrepareModel(JythonAntTask prepareModel) {
		this.prepareModel = prepareModel;
	}

	public String getProjectAttribute() {
		return projectAttribute;
	}

	public void setProjectAttribute(String projectAttribute) {
		this.projectAttribute = projectAttribute;
	}

	public File getProjectFile() {
		return projectFile;
	}

	public void setProjectFile(File projectFile) {
		this.projectFile = projectFile;
	}

	public long getProjectFileLastModified() {
		return projectFileLastModified;
	}

	public void setProjectFileLastModified(long projectFileLastModified) {
		this.projectFileLastModified = projectFileLastModified;
	}

	public TemplateNodeModel getProjectNode() {
		return projectNode;
	}

	public void setProjectNode(TemplateNodeModel projectNode) {
		this.projectNode = projectNode;
	}

	public TemplateModel getProjectTemplate() {
		return projectTemplate;
	}

	public void setProjectTemplate(TemplateModel projectTemplate) {
		this.projectTemplate = projectTemplate;
	}

	public TemplateModel getPropertiesTemplate() {
		return propertiesTemplate;
	}

	public void setPropertiesTemplate(TemplateModel propertiesTemplate) {
		this.propertiesTemplate = propertiesTemplate;
	}

	public long getTemplateFileLastModified() {
		return templateFileLastModified;
	}

	public void setTemplateFileLastModified(long templateFileLastModified) {
		this.templateFileLastModified = templateFileLastModified;
	}

	public String getTemplateName() {
		return templateName;
	}

	public void setTemplateName(String templateName) {
		this.templateName = templateName;
	}

	public TemplateModel getUserPropertiesTemplate() {
		return userPropertiesTemplate;
	}

	public void setUserPropertiesTemplate(TemplateModel userPropertiesTemplate) {
		this.userPropertiesTemplate = userPropertiesTemplate;
	}

	public String getEncoding() {
		return encoding;
	}

	public String getExtension() {
		return fileSuffix;
	}

	public String getModels() {
		return models;
	}

	public File getTemplateDir() {
		return templateDir;
	}

	public String getTemplateEncoding() {
		return templateEncoding;
	}

	public boolean isValidation() {
		return validation;
	}

	public String getProcesser() {
		return processer;
	}

	public void setProcesser(String processClass) {
		this.processer = processClass;
	}

	public void setFileMatch(String fileMatch) {
		this.fileMatch = fileMatch;
	}

	public String getFileMatch() {
		return fileMatch;
	}
}
